Container und IPv6 – Basics zum Verständnis / Netzwerkzuschnitt

In diesem Beitrag erläutere ich erst einmal die grundlegenden Probleme welche sich beim Einsatz von Container-Lösungen im Netzwerk ergeben und wie man sie sinnvoller Weise technisch lösen kann. Schlüssel hierfür ist die Verwendung von IPv6 anstelle von IPv4. In einem weiteren Artikel beschreibe ich dann die konkrete Umsetzung. Wer sich mit IPv6 und Containern bereits sicher fühlt kann diesen Artikel ggf. überspringen.

Container sind der aktuelle Stand der Technik was die Auslieferung und die Laufzeitumgebung für Serversoftware betrifft. Die Para-Virtualisierung löst einige Probleme die man typischer Weise hatte, unter anderem das Alles-oder-Nichts-Problem, wenn es darum geht Systemsoftware zu aktualisieren. Mit jedem größeren Versionssprung gibt es die Gefahr, dass bestimmte Teile des eigenen Codes nicht mehr laufen. Aktuell gibt es immer noch genügend Projekte und Software welche auf ältere (um nicht zu sagen teilweise uralte) Versionen von PHP angewiesen sind um zu funktionieren. Mit dem Versionssprung von 5.x nach 7.0 sind einige Altlasten und Funktionen über Bord geworfen worden, unter anderem auch der gesamte Stack für die MySQL-Anbindung, was einigen Anwendungen dann doch das Genick bricht. Häufig stand man nun vor dem Problem, für einen Teil des gehosteten Angebots die Version anheben zu wollen, aber die Altlasten müsste man dafür erst einmal auf Stand bringen. Das ist nicht immer ohne weiteres möglich. Mit Containern schafft man sich hier ggf. kleinere Services mit jeweils ihrer maßgeschneiderten Umgebung – inklusive abgeschottetem Datenbankserver, den sich nicht mehr alle Angebote teilen müssen (über die Vor- und Nachteile von Datenbanken in Containern kann man einen eigenen Artikel schreiben).

Hier geht es aber erstmal um ein anderes Problem beim Einsatz von verschiedenen (Micro-)Services auf einer Maschine: Im Regelfall hat der Server nämlich genau eine externe IP-Adresse als Anbindung an die große weite Welt des Internets. Mit der häufigste nach außen angebotene Service ist ein Webserver, welcher HTTP und hoffentlich auch HTTPS spricht und dafür die Ports 80 und 443 nutzt. Das Konzept der „Virtual Hosts“ (kurz VHosts) ist bereits sehr alt und erprobt, seit sich der Mechanismus „Server Name Indication“ (SNI) durchgesetzt hat, kann man damit auch mehrere Domains mit Verschlüsselung anbieten. Einer der Gründe für SNI ist übrigens die Verknappung der vorhandenen IPv4-Adressen.

Mit Docker bekommt man zwar nahezu vollständige Umgebungen virtualisiert, aber die IPv4-Adressen werden damit auch nicht mehr, und mittlerweile sind diese ein teures Gut geworden. Technisch ist es nicht möglich, dass sich die verschiedenen Docker-Instanzen die IP-Adresse und den Port der Host-Maschine teilen. Schlimmer noch, die Docker-Instanzen laufen in einem eigenen Netzwerk (in der Standard-Konfiguration 172.16.0.0/12). Die privaten Adressbereiche sind weltweit nicht eindeutig und daher im freien Internet nicht verwendbar (die meisten kennen die kleineren privaten Netzwerke wie etwa 192.168.0.0/16). Damit sind die Instanzen erst einmal recht gut nach außen abgeschottet, vergleichbar dem was man heute häufig als Heimnetzwerk mit Router kennt (im Prinzip nur spiegelverkehrt). Man kann mit Docker durchaus einzelne Container nach außen exponieren, im Wesentlichen sorgt Docker dann dafür, dass auf der Hostmaschine eine Firewall-Regel Source-NAT bzw. Portforwarding eingerichtet wird. Für viele Heimadmins ein alter Hut, und nur durch Docker etwas automatisiert und versteckt. Allerdings gilt auch hier: ein Port nach draußen kann genau auf eine Maschine intern weitergeleitet werden.

Es bedarf also einer weiteren Software, dafür gibt es eine mehr oder weniger schöne Lösung, die sich Reverse-Proxy nennt. Im Wesentlichen ist es ein Serverdienst, der anhand der gewünschten Domain an den richtigen internen Server weiter leitet. Oftmals kombiniert man das dann auch mit einem Load-Balancer um Redundanz zu schaffen bzw. die Last auf mehrere Hintergrund-Maschinen halbwegs gleichmäßig zu verteilen. Allerdings ist das eigentlich keine wirklich brauchbare Lösung – denn man muss auf dem Loadbalancer ja im Zweifel auch noch erst einmal die SSL-Verbindung terminieren, oder zumindest einmal die per ServerNameIndication (SNI) mitgelieferte Wunsch-Adresse des Clients ermitteln. Das Ende-zu-Ende-Prinzip wird auf alle Fälle aufgebrochen, vom Aufwand der Verschlüsselung und Neu-Verschlüsselung zum tatsächlich adressierten Host einmal ganz abgesehen. Am ärgerlichsten ist aber, dass man den Reverseproxy auch noch jedes Mal getrennt konfigurieren muss bzw. ihm Änderungen an den Weiterleitungsregeln übermitteln muss (für welche Adressen soll er zuständig sein und welche wohin weiter leiten) – das kann man partiell automatisieren aber es ist immer ein gewisser Verwaltungsaufwand zusätzlich notwendig.

Treten wir nochmal einen Schritt weiter zurück und betrachten uns das OSI-Modell einmal genauer – in diesem etablierten Schichtenmodell gibt es bereits Mechanismen welche sich um das Routing kümmern (Schicht 3). Der Reverserproxy liegt also eigentlich einige Ebenen zu weit oben angesiedelt und macht dann (notgedrungen) Aufgaben die eigentlich auf der Netzwerkschicht schon gemacht werden sollten. Es ist insgesamt aktuell ein für mich ärgerlicher und unverständlicher Trend weshalb man in ganz vielen Fällen in der Applikationsebene Routing einbaut oder sogar die Logik zu Übertragungssicherheit und Übertragungssteuerung (Schicht 4) in die Applikationsschicht verschiebt wie etwa bei HTTP3 angedacht – aber das ist ein anderes Thema zu dem ich bei Gelegenheit mal noch einen weiteren Eintrag schreiben werde.

Nun gut was wollen wir eigentlich: Wir haben mehrere getrennte Systeme und leider extern nur eine IPv4 und wir würden das Routing gerne auf Ebene 3 durchführen wo es hingehört. Die einfachste Lösung wäre ja, man besorgt sich IPv4-Adressen, lange Zeit war das kein Problem aber mittlerweile sind sie ja nicht mehr so ohne weiteres verfügbar. Aber halt – es gibt doch für dieses Problem bereits seit geraumer Zeit eine Lösung, die sich nur noch nicht auf breiter Front hat durchsetzen können: IPv6.

Nicht nur dass der Adressraum damit deutlich größer wird (128 Bit), nein IPv6 hat von Anfang an vorgesehen, dass ein Rechner nicht mehr länger nur eine IPv6-Adresse erhält sondern im Regel gleich einen eigenen Netzbereich. Ob es jetzt pro Knoten gleich 64 Bit hätten sein müssen, darüber lässt sich sicherlich streiten aber prinzipiell hat ein Host damit mehr als genügend Adressen zur Verfügung. Die meisten großen Hosting-Anbieter teilen /64 Netzwerke zu, im privat-Anschlussbereich gibt es für die angeschlossenen Heimnetzwerke meist sogar /56er-Segmente.

Im Wesentlichen muss man jetzt noch ein wenig Netzwerk-Verteilung machen, also planen wie man die vielen zur Verfügung stehenden IP-Adressen für die eigenen Zwecke sinnvoll nutzt. Das wird von Nutzer zu Nutzer und von Szenario zu Szenario etwas unterschiedlich ausfallen, daher beschreibe ich es hier nur grob: Man benötigt einen Adressbereich für die Host-Maschine denn diese muss man ja ansprechen können um den Container-Dienst (vulgo Docker) einrichten und starten zu können. Zudem gibt es Services die man vielleicht nicht unbedingt in einen Container verpacken will bzw. man möchte es ggf. schrittweise machen.

Im folgenden verwende ich die IPv6-Adressen aus dem Dokumentationsvorrat (2001:db8::/32 ) um eine mögliche Architektur zu beschreiben. Ich gehe dabei davon aus, dass wir einen etwas geizigen Hoster haben der uns „nur“ ein /64-Prefix pro Server zuweist, mit einem /56 Netzwerk kann man technisch genauso verfahren. Wichtig ist immer, dass man sich ungefähr ein Bild der Größe des Netzwerk-Segments macht. /64 bedeutet, das man theoretisch 2^64-Adressen zur Verfügung hat (18446744073709551616  = 1,8*10^19) selbst wenn man da einige davon nicht technisch nutzen kann, ist das immer noch ein sehr großer Raum um sich auszutoben. Wir nehmen also an, dass der Hoster uns folgendes Prefix zugewiesen hat: 2001:db8:dada:dada/64 (ich verwende absichtlich nicht ein Prefix mit aufgefüllten Nullen, darunter leidet in Kombination mit den verkürzten Schreibweisen die Verständlichkeit doch ganz ordentlich). Zudem gibt es im Netz leider noch einige Verwirrung bzw. große Unsicherheit bei den Netzwerkgrößen: In vielen Fällen wird darauf verwiesen das jeder Endpunkt im Netz immer mindestens ein /64er Präfix bekommen muss, was schlichtweg falsch ist. IPv6 ist derart gestaltet, dass es keine deratigen Regeln benötigt, man kann Netzsegmente so klein machen wie man möchte bis hin zu /128 was dann exakt einer Adresse spricht und dann kann man tatsächlich nicht mehr von einem Netz sprechen, sinvoll zu gebrauchen sind daher Größen ab /126 (entsprechend 2 Bit nutzbar => maximal 4 Adressen). Im Heimnetzbereich übliche IPv4-Netzwerke haben in der Regel bereits eine Größe von IPv4/24, also 8 Bit zur Host-Adressierung, entsprechend maximal 256 Adressen (wovon einige spezielle Aufgaben wie Broadcast haben also nicht effektiv genutzt werden können) – im IPv6-Bereich würde man hier von einem /120-Netz sprechen.

Woher kommt dann diese „komische“ Auffassung über die /64-Notwendigkeit im Bereich IPv6? Dies hat mit einem Feature von IPv6 zu tun, das man gerne nutzen möchte: Stateless Address Auto Configuration (SLAAC) – mit dieser Funktion ist es möglich, dass jeder Client sich selbst anhand einiger Rahmenbedingungen seine eigene IPv6-Adresse im aktuellen Netzwerk errechnet. Dazu wird die meist ohnehin vorhandene MAC-Adresse der Hardware (oder deren virtualisiertem Äquivalent) heran gezogen. Dies geht davon aus, dass die MAC-Adresse eines Geräts weltweit eindeutig sein sollte (was leider bei billiger Hardware auch mal nicht der Fall ist …) diese hat 48 Bit und sieht in ihrer Schreibweise einer IPv6-Adresse nicht unähnlich (was die Verwirrung bei Einsteigern noch weiter erhöht). Mittels des EUI-Algorithmus (ganz kurz: in der Mitte FF:FE einfügen, zweites Bit von vorne kippen) werden daraus die benötigten 64Bit. SLAAC ist eine nette Einrichtung für dynamische Netzwerke und dient vor allem der Ermittlung der ersten (link-lokalen)-IPv6-Adresse über die dann die weitere Konfiguration abgewickelt werden kann. Im Wesentlichen hat man für die globale Adresse dann die Möglichkeit: manuell, SLAAC oder DHCPv6. Manuell ist nur sinnvoll wenn die Zuweisung des Präfix sich nicht regelmäßig ändert, bei Servern in Rechnenzentren ist das in der Regel gegeben. SLAAC ist hilfreich wenn es schnell gehen muss bzw. man nicht viel Einrichtungsaufwand haben möchte: Der Router gibt das Präfix bekannt, den Rest erledigt der Client. DHCPv6 kennen viele durch das IPv4-Äquivalent: Hier kümmert sich der Router um die Zuweisung einer IP-Adresse an den Client (stateful, es wird „Buch geführt“).  DHCPv6 ist vor allem in Netzwerken interessant wenn anhand bestimmter Regeln Untersegmente gebildet werden sollen (z.B. Gäste in ein abgetrenntes Netzsegment, Aufteilung des IP-Bereichs nach Zuständigkeiten oder Lokalitäten wie etwa Gebäude und Stockwerksnummer). Für die Aufteilung auf dem Server teilen wir uns das Netz in kleinere Segmente auf, laut Doku empfiehlt sich für Docker die Bereiche nicht kleiner als /80 zu wählen, dann ist da Umrechnen der virtuellen MAC-Adresse des Containers recht einfach, sie wird einfach angehängt.

Eine mögliche Konfiguration / Aufteilung sieht dann ggf. wie folgt aus:

Segment Prefixlänge Nutzung
2001:db8:dada:dada:0000 /68 Host-Maschine
2001:db8:dada:dada:d000 /68 Docker /Containerbereich
2001:db8:dada:dada:d001 /80 Docker-Projekt A
2001:db8:dada:dada:d002 /80 Docker-Projekt B
2001:db8:dada:dada:d003 /80 Docker-Projekt C

Die weiteren möglichen Adressbereich habe ich erst mal nicht verwendet => die einzelnen Dockerprojekte haben 2^48-Adressmöglichkeiten (281.474.976.710.656 Adressen) damit sollte es selbst bei größeren Projekten erst einmal keine Probleme geben, wenn doch muss man die Bereiche größer wählen.

Soweit einmal die generelle Struktur – wie immer gilt: „Your milage may vary“, bzw. „adjust to taste“ – man sollte sich definitiv die Zeit nehmen und sich über die Aufteilung und Verwaltung des eigenen Netzbereichs Gedanken machen und für alle Fälle etwas Luft einplanen, wie gesehen sind einzelne Subnetze mit /80 immer noch ausreichend umfangreich.

Die nächsten Schritte die es zu machen gilt sind:

  • Hostmaschine mit IPv6 ausrüsten (falls noch nicht geschehen)
  • ggf. Routing und Firewall einrichten
  • Docker IPv6 beibrigen bzw. einschalten
  • Container IPv6-fähig machen und ggf. auf Absicherung achten
  • als Fallback für IPv4-Anfragen ggf. doch eine Art Load-Balancer / Reverseproxy aufsetzen der aus IPv4 intern IPv6 macht.

Diese Schritte werde ich in loser Folge in weiteren Artikeln hier beschreiben.